﻿#region COPYRIGHT
//--------------------------------------------------------------------------------
// <copyright file="CollisionManager.cs" company="starLiGHT Entertainment Studios">
//    Copyright (c) 2010, 2011
//       Roland Rosenkranz (Glatzemann@email.de)
// </copyright>
// <license>
//   This file is part of starLiGHT.Collision.
//
//   starLiGHT.Collision is free software: you can redistribute it and/or modify
//   it under the terms of the GNU Lesser General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   starLiGHT.Collision is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU Lesser General Public License for more details.
//
//   You should have received a copy of the GNU Lesser General Public License
//   along with starLiGHT.Collision. If not, see http://www.gnu.org/licenses/.
//    
//   ADDITIONAL (commercial) LICENSES for starLiGHT.Collision are available on request.
// </license>
// <version>
// **************[ starLiGHT.Engine SVN ]**********************
// * $Rev:: 3410                  $:  Revision of last commit *
// * $Author:: unknown            $:  Author of last commit   *
// * $Date:: 2011-01-14 10:46:25 #$:  Date of last commit     *
// ************************************************************
// </version>
//--------------------------------------------------------------------------------
#endregion

namespace starLiGHT.Collision
{
    // http://www.metanetsoftware.com/technique/tutorialB.html#section0

    //////////TODO: add 3d-handling to update method

    #region Using Statements
    using System;
    using System.Collections.Generic;
    using System.Text;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using starLiGHT.Collections;
    using starLiGHT.Collision.BroadPhase;
    using starLiGHT.Collision.NarrowPhase;
    #endregion

    public class CollisionManager
    {
        #region Member Variables
        private IBroadPhase broadPhase;
        private INarrowPhase narrowPhase;
        private ICollisionDebugDraw debugDraw;
        private PairManager<ICollidable> pairManager;

        private ICollidable[] collidables;

        private Dictionary<int, bool[]> colorArrayCache;

        private bool worldLocked;
        private FastList<ICollidable> collidablesToRemove;
        #endregion

        public CollisionManager()
        {
            this.colorArrayCache = new Dictionary<int, bool[]>();
            this.collidablesToRemove = new FastList<ICollidable>();
        }

        public CollisionManager(BroadPhaseType broadPhaseType, NarrowPhaseType narrowPhaseType)
            : this()
        {
            BroadPhaseType = broadPhaseType;
            NarrowPhaseType = narrowPhaseType;
        }

        public CollisionManager(BroadPhaseType broadPhaseType, NarrowPhaseType narrowPhaseType, bool drawBoundingBoxes, bool drawBoundingCircles, bool drawBoundingHulls)
            : this(broadPhaseType, narrowPhaseType)
        {
            this.DebugDraw = new DefaultCollisionDebugDraw(drawBoundingBoxes, drawBoundingCircles, drawBoundingHulls);
        }

        #region Events
        public event EventHandler<CollisionEventArgs> Collision;

        public event EventHandler<CollisionResolvedEventArgs> CollisionResolved;

        #endregion

        public IBroadPhase BroadPhase
        {
            get { return this.broadPhase; }
            set { this.broadPhase = value; }
        }

        public BroadPhaseType BroadPhaseType
        {
            get
            {
                switch (this.BroadPhase.GetType().Name)
                {
                    case "AxisAlignedBoundingBoxBruteForce":
                        return BroadPhaseType.AxisAlignedBoundingBoxBruteForce;
                    case "AxisAlignedRectangleBruteForce":
                        return BroadPhaseType.AxisAlignedRectangleBruteForce;
                    case "BoundingCircleBruteForce":
                        return BroadPhaseType.BoundingCircleBruteForce;
                    case "QuadTree":
                        return BroadPhaseType.QuadTree;
                    case "SweepAndPrune":
                        return BroadPhaseType.SweepAndPrune;
                }

                throw new NotImplementedException("unknown BroadPhaseType in CollisionManager.BroadPhaseType");
            }

            set
            {
                switch (value)
                {
                    case BroadPhaseType.AxisAlignedBoundingBoxBruteForce:
                        this.BroadPhase = new AxisAlignedBoundingBoxBruteForce();
                        break;
                    case BroadPhaseType.AxisAlignedRectangleBruteForce:
                        this.BroadPhase = new AxisAlignedRectangleBruteForce();
                        break;
                    case BroadPhaseType.BoundingCircleBruteForce:
                        this.BroadPhase = new BoundingCircleBruteForce();
                        break;
                    case BroadPhaseType.QuadTree:
                        this.BroadPhase = new QuadTree();
                        break;
                    case BroadPhaseType.SweepAndPrune:
                        this.BroadPhase = new SweepAndPrune();
                        break;
                }
            }
        }

        public INarrowPhase NarrowPhase
        {
            get { return this.narrowPhase; }
            set { this.narrowPhase = value; }
        }

        public NarrowPhaseType NarrowPhaseType
        {
            get
            {
                switch (this.NarrowPhase.GetType().Name)
                {
                    case "SeparatingAxisTheorem":
                        return NarrowPhaseType.SeparatingAxisTheorem;
                    case "PerPixelNarrowPhase":
                        return NarrowPhaseType.PerPixel;
                }

                throw new NotImplementedException("unknown NarrowPhaseType in CollisionManager.NarrowPhaseType");
            }

            set
            {
                switch (value)
                {
                    case NarrowPhaseType.PerPixel:
                        this.NarrowPhase = new PerPixelNarrowPhase();
                        break;
                    case NarrowPhaseType.SeparatingAxisTheorem:
                        this.NarrowPhase = new SeparatingAxisTheorem();
                        break;
                }
            }
        }

        public ICollisionDebugDraw DebugDraw
        {
            get { return this.debugDraw; }
            set { this.debugDraw = value; }
        }

        public PairManager<ICollidable> PairManager
        {
            get 
            {
                if (this.pairManager == null)
                {
                    this.pairManager = new PairManager<ICollidable>();
                }

                return this.pairManager; 
            }
        }

        public void AddCollidable(ICollidable collidable)
        {
            if (this.broadPhase != null && !this.broadPhase.CheckCompatibility(collidable))
            {
                throw new NotSupportedException("The collidable is not supported by the BroadPhase.");
            }

            if (this.narrowPhase != null && !this.narrowPhase.CheckCompatibility(collidable))
            {
                throw new NotSupportedException("The collidable is not supported by the NarrowPhase.");
            }

            if (this.broadPhase != null)
            {
                this.broadPhase.RegisterObject(collidable as ICollidable2D);
            }

            if (this.collidables == null)
            {
                this.collidables = new ICollidable[5];
            }

            for (int i = 0; i < this.collidables.Length; i++)
            {
                if (this.collidables[i] == null)
                {
                    this.collidables[i] = collidable;
                    return;
                }
            }

            int length = this.collidables.Length;
            ICollidable[] tmpCollidables = new ICollidable[length * 2];
            Array.Copy(this.collidables, tmpCollidables, length);
            this.collidables = tmpCollidables;
            this.collidables[length] = collidable;
        }

        public void RemoveCollidable(ICollidable collidable)
        {
            if (!this.worldLocked)
            {
                for (int i = 0; i < this.collidables.Length; i++)
                {
                    if (this.collidables[i] != null && this.collidables[i].Equals(collidable))
                    {
                        this.collidables[i] = null;

                        // remove all pairs which contain the removed collidable
                        foreach (UserPair<ICollidable> pair in this.PairManager.Pairs)
                        {
                            if (pair.object0.Equals(collidable) || pair.object1.Equals(collidable))
                            {
                                this.PairManager.RemovePair(pair.ID0, pair.ID1);
                            }
                        }

                        break;
                    }
                }
            }
            else
            {
                this.collidablesToRemove.Add(collidable);
            }
        }

        public void CollisionMaskFromCache(Texture2D texture, out bool[] targetArray)
        {
            if (texture == null)
            {
                throw new ArgumentNullException("texture");
            }

            int textureHash = texture.GetHashCode();
            if (this.colorArrayCache.ContainsKey(textureHash))
            {
                targetArray = this.colorArrayCache[textureHash];
            }
            else
            {
                Color[] colors = new Color[texture.Width * texture.Height];
                texture.GetData<Color>(colors);

                targetArray = new bool[texture.Width * texture.Height];

                for (int i = 0; i < colors.Length; i++)
                {
                    targetArray[i] = colors[i].A > 0;
                }
                
                this.colorArrayCache[textureHash] = targetArray;
            }
        }

        public void Update()
        {
            this.worldLocked = true;

            if (this.broadPhase != null)
            {
                // *********************
                // * cleanup old pairs *
                // *********************
                this.broadPhase.CleanupPairs(this);

                // **********************************************
                // * build collision pairs using the broadPhase *
                // **********************************************
                this.broadPhase.BuildCollisionPairs(this, ref this.collidables);
            }

            if (this.narrowPhase != null && this.broadPhase != null && this.PairManager != null && this.pairManager.Count > 0)
            {
                // check each collision pair from broadphase in detail
                this.narrowPhase.CollisionCount = 0;
                if (this.pairManager != null)
                {
                    foreach (UserPair<ICollidable> pair in this.PairManager.Pairs)
                    {
                        CollisionManifold<ICollidable> manifold = this.narrowPhase.ProcessPair(this, pair);
                        if (manifold != null && manifold.NarrowPhaseCollision)
                        {
                            this.narrowPhase.CollisionCount++;
                        }
                    }
                }
            }
            else if (this.narrowPhase != null && this.broadPhase == null)
            {
                // narrowPhase only

                //////////TODO: test + optimize

                // first, create virtual pairs
                ICollidable a, b;

                for (ushort i = 0; i < this.collidables.Length; i++)
                {
                    a = this.collidables[i];
                    if (a != null)
                    {
                        for (ushort j = (ushort)(i + 1); j < this.collidables.Length; j++)
                        {
                            b = this.collidables[j];
                            if (b != null)
                            {
                                this.PairManager.AddPair(i, j, a, b);
                            }
                        }
                    }
                }

                // check each collision pair from broadphase in detail
                foreach (UserPair<ICollidable> p in this.PairManager.Pairs)
                {
                    this.narrowPhase.ProcessPair(this, p);
                }
            }

            this.worldLocked = false;

            // remove queued objects
            foreach (ICollidable c in this.collidablesToRemove)
            {
                this.RemoveCollidable(c);
            }

            this.collidablesToRemove.Clear();
        }

        public void DrawDebug(SpriteBatch spriteBatch)
        {
            if (this.debugDraw != null && this.collidables != null)
            {
                foreach (ICollidable c in this.collidables)
                {
                    if (c is ICollidable2D)
                    {
                        ICollidable2D c2d = c as ICollidable2D;
                        this.debugDraw.DrawBoundingBox(spriteBatch, c2d);
                        this.debugDraw.DrawBoundingCircle(spriteBatch, c2d);
                        this.debugDraw.DrawBoundingHull(spriteBatch, c2d);
                    }
                    else if (c is ICollidable3D)
                    {
                        throw new NotImplementedException("DebugDraw for ICollidable3D not implemented");
                    }
                }

                this.debugDraw.DrawUserRectangles(spriteBatch);

                this.debugDraw.DrawUserLines(spriteBatch);
            }
        }

        public void OnCollision(UserPair<ICollidable> pair, CollisionManifold<ICollidable> manifold)
        {
            if (this.Collision != null)
            {
                this.Collision(this, new CollisionEventArgs(pair, manifold));
            }
        }

        public void OnCollisionResolved(UserPair<ICollidable> pair)
        {
            if (this.CollisionResolved != null)
            {
                this.CollisionResolved(this, new CollisionResolvedEventArgs(pair));
            }
        }
    }
}
